﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Foundation;
using Windows.Security.Cryptography.Core;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Shapes;
using CustomControls.Converters;
using CustomControls.Enums;
using CustomControls.ExtendedSegments;

namespace CustomControls.Controls
{
    /// <summary>
    /// A control used to animate children along a path
    /// </summary>
    [ContentProperty(Name = "Children")]
    public partial class LayoutPath : ContentControl
    {
        #region private variables

        /// <summary>
        /// The grid containing all child wrappers that are moved along path
        /// </summary>
        private Grid CHILDREN;

        /// <summary>
        /// Top level container used for scaling <see cref="LayoutPath"/>
        /// </summary>
        private Viewbox VIEW_BOX;

        /// <summary>
        /// The path object, generated by specified <see cref="Path"/> property. It is used for previewing path.
        /// </summary>
        private Path PATH;

        private readonly ObservableCollection<object> _children = new ObservableCollection<object>();

        #endregion

        #region public properties

        /// <summary>
        /// Children that are positioned along <see cref="Path"/>
        /// </summary>
        public IList<object> Children => _children;

        /// <summary>
        /// The extended geometry, mainly used for getting point at fraction length
        /// </summary>
        public ExtendedPathGeometry ExtendedGeometry { get; private set; }

        #endregion

        #region initialization

        protected override void OnApplyTemplate()
        {
            VIEW_BOX = GetTemplateChild(nameof(VIEW_BOX)) as Viewbox;
            PATH = GetTemplateChild(nameof(PATH)) as Path;
            CHILDREN = GetTemplateChild(nameof(CHILDREN)) as Grid;

            PATH.SetBinding(Windows.UI.Xaml.Shapes.Path.DataProperty, new Binding()
            {
                Path = new PropertyPath("Path"),
                Source = this
            });

            VIEW_BOX.SetBinding(Viewbox.StretchProperty, new Binding()
            {
                Path = new PropertyPath("Stretch"),
                Source = this
            });

            PATH.Opacity = PathVisibility == Visibility.Visible ? 0.5 : 0;

            if (ExtendedGeometry != null)
                PATH.Margin = new Thickness(-ExtendedGeometry.PathOffset.X, -ExtendedGeometry.PathOffset.Y, 0, 0);

            foreach (var child in _children)
                CHILDREN.Children.Add(new LayoutPathChildWrapper(child as FrameworkElement, ChildrenAlignment, ChildrenOrientation));

            //TODO: _children.Clear does not invoke this event.
            _children.CollectionChanged += ChildrenOnCollectionChanged;

            base.OnApplyTemplate();
        }

        private async void ChildrenOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
        {
            if (args.Action == NotifyCollectionChangedAction.Reset)
            {
                CHILDREN.Children.Clear();
                return;
            }

            if (args.OldItems != null)
            {
                foreach (var child in args.OldItems)
                {
                    var wrapper = CHILDREN.Children.FirstOrDefault(x => ((LayoutPathChildWrapper)x).Content == child);
                    if (wrapper != null)
                    {
                        CHILDREN.Children.Remove(wrapper);
                        ((LayoutPathChildWrapper)wrapper).Content = null;
                    }
                }
            }

            if (args.NewItems != null)
            {
                foreach (var child in args.NewItems)
                {
                    var wrapper = CHILDREN.Children.FirstOrDefault(x => ((LayoutPathChildWrapper)x).Content == child);
                    if (wrapper == null)
                        CHILDREN.Children.Insert(args.NewStartingIndex, new LayoutPathChildWrapper(child as FrameworkElement, ChildrenAlignment, ChildrenOrientation));
                }
            }

            //Force UI to render elements. If not, width and height are giving 0 values. 
            //Dimensions are needed for correctly aligning items.
            await Task.Delay(1);
            TransformToProgress(PathProgress);
        }

        public LayoutPath()
        {
            DefaultStyleKey = typeof(LayoutPath);

            Loaded += delegate
            {
                TransformToProgress(PathProgress);
            };
        }

        public LayoutPath(PathGeometry pathGeometry)
        {
            DefaultStyleKey = typeof(LayoutPath);
            Path = pathGeometry;

            Loaded += delegate
            {
                TransformToProgress(PathProgress);
            };
        }

        public LayoutPath(string pathMarkup)
        {
            DefaultStyleKey = typeof(LayoutPath);
            Path = new StringToPathGeometryConverter().Convert(pathMarkup);

            Loaded += delegate
            {
                TransformToProgress(PathProgress);
            };
        }

        #endregion

        #region dependency properties callbacks

        private static void OrientationChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var sender = ((LayoutPath)o);
            if (sender.CHILDREN != null)
            {
                foreach (LayoutPathChildWrapper child in sender.CHILDREN.Children)
                {
                    child.UpdateAlignment(sender.ChildrenAlignment, sender.ChildrenOrientation);
                }
            }
            UpdateRotation(o, e);
        }

        private static void ChildAlignmentChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var sender = ((LayoutPath)o);
            if (sender.CHILDREN != null)
            {
                foreach (LayoutPathChildWrapper child in sender.CHILDREN.Children)
                {
                    child.UpdateAlignment((ChildAlignment)e.NewValue, sender.ChildrenOrientation);
                }
            }
            TransformToProgress(o, e);
        }

        private static void PathChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var data = (Geometry)e.NewValue;
            var sen = ((LayoutPath)o);
            sen.ExtendedGeometry = new ExtendedPathGeometry(data as PathGeometry);
            if (sen.PATH != null)
                sen.PATH.Margin = new Thickness(-sen.ExtendedGeometry.PathOffset.X, -sen.ExtendedGeometry.PathOffset.Y, 0, 0);
            sen.TransformToProgress(((LayoutPath)o).PathProgress);
        }

        private static void PathVisibleChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var path = ((LayoutPath)o).PATH;
            //we don't collapse path because we need it's space for stretching control.
            if (path != null)
                path.Opacity = (Visibility)e.NewValue == Visibility.Visible ? 0.5 : 0;
        }

        private static void ProgressChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ((LayoutPath)o).TransformToProgress((double)e.NewValue);
        }

        private static void AttachedProgressPropertyChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            LayoutPathChildWrapper wrapper = null;
            while (true)
            {
                o = VisualTreeHelper.GetParent(o);
                if (o is LayoutPathChildWrapper)
                    wrapper = (LayoutPathChildWrapper)o;

                if (o is LayoutPath)
                {

                    ((LayoutPath)o).MoveChild(wrapper, (double)e.NewValue, !DesignMode.DesignModeEnabled);
                    return;
                }
                if (o == null)
                    return;
            }
        }

        private static void AttachedProgressOffsetPropertyChangedCallback(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            LayoutPathChildWrapper wrapper = null;
            while (true)
            {
                o = VisualTreeHelper.GetParent(o);
                if (o is LayoutPathChildWrapper)
                    wrapper = (LayoutPathChildWrapper)o;

                if (o is LayoutPath)
                {
                    if (DesignMode.DesignModeEnabled)
                    {
                        ((LayoutPath)o).TransformToProgress(((LayoutPath)o).PathProgress);
                        return;
                    }

                    ((LayoutPath)o).MoveChild(wrapper, wrapper.Progress + ((double)e.NewValue / 100.0), !DesignMode.DesignModeEnabled);
                    return;
                }
                if (o == null)
                    return;
            }
        }

        private static void TransformToProgress(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ((LayoutPath)o).TransformToProgress(((LayoutPath)o).PathProgress);
        }

        private static void UpdateRotation(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var path = (LayoutPath)o;
            if (path.ExtendedGeometry == null || path.CHILDREN == null)
                return;
            var children = path.CHILDREN.Children.ToArray();
            for (int i = 0; i < children.Count(); i++)
            {
                var wrapper = (LayoutPathChildWrapper)children[i];
                var progress = wrapper.Progress;
                Point childPoint;
                double rotationTheta;
                path.ExtendedGeometry.GetPointAtFractionLength(progress, out childPoint, out rotationTheta);
                path.Rotate(wrapper, rotationTheta, false);
            }
        }

        #endregion

        #region methods

        /// <summary>
        /// Call this to reset smoothing and position items to <see cref="PathProgress"/>. Useful when there is no active storyboard for applying changes.
        /// </summary>
        /// <param name="progress">Reset to a specific <see cref="PathProgress"/> value.</param>
        public void ResetSmoothingAndRefresh(double? progress = null)
        {
            if (progress.HasValue)
                PathProgress = progress.Value;
            TransformToProgress(PathProgress, false);
        }

        private void TransformToProgress(double progress, bool smooth = true)
        {
            if (ExtendedGeometry == null || CHILDREN == null)
                return;
            if (DesignMode.DesignModeEnabled)
                smooth = false;

            var children = CHILDREN.Children.ToArray();

            for (int i = 0; i < children.Count(); i++)
            {
                double childPercent = progress - (i * ChildrenPadding);

                var wrapper = (LayoutPathChildWrapper)children[i];
                if (!GetIsMovable((UIElement)wrapper.Content))
                    continue;

                var childProgress = GetProgress((UIElement)wrapper.Content);
                if (!double.IsNaN(childProgress))
                    childPercent = childProgress;

                var childOffset = GetProgressOffset((UIElement)wrapper.Content);
                childPercent += childOffset;

                MoveChild(wrapper, childPercent, smooth);
            }

            var tmp = ExtendedGeometry.GetPointAtFractionLength(progress);
            CurrentPosition = tmp.Item1;
            CurrentRotation = tmp.Item2;

            CurrentLength = ExtendedGeometry.PathLength * (progress / 100.0);
        }

        private void MoveChild(LayoutPathChildWrapper wrapper, double childPercent, bool smooth)
        {
            wrapper.RawProgress = childPercent;
            ApplyStackFilters(ref childPercent, wrapper);

            if (ChildrenEasingFunction != null)
            {
                childPercent = ChildrenEasingFunction.Ease(childPercent / 100.0) * 100;
                wrapper.RawProgress = ChildrenEasingFunction.Ease(wrapper.RawProgress / 100.0) * 100;
            }

            Point childPoint;
            double rotationTheta;
            ExtendedGeometry.GetPointAtFractionLength(childPercent, out childPoint, out rotationTheta);

            wrapper.Progress = childPercent;
            
            Rotate(wrapper, rotationTheta, smooth);
            Translate(wrapper, childPoint, smooth);
        }

        private void ApplyStackFilters(ref double progress, LayoutPathChildWrapper wrapper)
        {
            if (progress < 0)
            {
                if (!ExtendedGeometry.PathGeometry.Figures.First().IsClosed)
                {
                    //avoid traveling form start to end when smoothing in enabled
                    wrapper.ForceNoSmoothOnProcessing();
                }

                if (StartBehavior == Behaviors.Collapse)
                {
                    wrapper.Visibility = Visibility.Collapsed;
                }
                else
                {
                    wrapper.Visibility = Visibility.Visible;
                    if (StartBehavior == Behaviors.Stack)
                    {
                        progress = 0;
                    }
                    else
                    {
                        //transfer to range 0-100.
                        //Examples
                        // -2 + 100 = 98 exit
                        // -102 + 100 = -2 + 100 = 98 exit
                        while (progress < 0)
                            progress = 100 + progress;
                    }
                }
            }
            else if (progress > 100)
            {
                if (!ExtendedGeometry.PathGeometry.Figures.First().IsClosed)
                {
                    //avoid traveling form end to start when smoothing in enabled
                    wrapper.ForceNoSmoothOnProcessing();
                }

                if (EndBehavior == Behaviors.Collapse)
                {
                    wrapper.Visibility = Visibility.Collapsed;
                }
                else
                {
                    wrapper.Visibility = Visibility.Visible;
                    if (EndBehavior == Behaviors.Stack)
                    {
                        progress = 99.9999;
                    }
                    else
                    {
                        //transfer to range 0-100
                        progress = progress % 100;
                        if (progress == 0)
                        {
                            progress = 99.9999;
                        }
                    }
                }
            }
            else
            {
                if (progress == 100)
                {
                    progress = 99.9999;
                }
                wrapper.Visibility = Visibility.Visible;
            }
        }

        private void Rotate(LayoutPathChildWrapper wrapper, double rotationTheta, bool smooth)
        {
            if (ChildrenOrientation == Orientations.None)
            {
                wrapper.Rotation = 0;
                return;
            }

            if (ChildrenOrientation == Orientations.Vertical)
                rotationTheta += 90;
            else if (ChildrenOrientation == Orientations.VerticalReversed)
                rotationTheta += 270;
            else if (ChildrenOrientation == Orientations.ToPathReversed)
                rotationTheta += 180;

            rotationTheta = rotationTheta % 360;


            var rotationSmoothing = RotationSmoothingDefault;
            var progressDistance = wrapper.ProgressDistance;
            if (smooth)
            {
                var childRotationSmoothing = GetRotationSmoothing((UIElement)wrapper.Content);
                if (!double.IsNaN(childRotationSmoothing))
                    rotationSmoothing = childRotationSmoothing;
            }

            //try smooth rotation
            if (smooth && !double.IsNaN(progressDistance) && rotationSmoothing > 0 && progressDistance > 0 && !wrapper.NoRotateSmoothForced)
            {
                var degreesDistance = Math.Max(rotationTheta, wrapper.Rotation) - Math.Min(rotationTheta, wrapper.Rotation);
                var rotation = wrapper.Rotation;
                while (degreesDistance > 180)
                {
                    if (rotationTheta > rotation)
                        rotation = rotation + 360;
                    else
                        rotation = rotation - 360;
                    degreesDistance = Math.Max(rotationTheta, rotation) - Math.Min(rotationTheta, rotation);
                }
                wrapper.Rotation = (rotation * rotationSmoothing * 0.2 + rotationTheta * progressDistance) / (rotationSmoothing * 0.2 + progressDistance);
            }
            else
            {
                wrapper.Rotation = rotationTheta;
                wrapper.NoRotateSmoothForced = false;
            }
        }

        private void Translate(LayoutPathChildWrapper wrapper, Point childPoint, bool smooth)
        {
            double translateX = childPoint.X, translateY = childPoint.Y;
            var wrappedChild = wrapper.Content as FrameworkElement;

            var translationSmoothing = TranslationSmoothingDefault;
            var progressDistance = wrapper.ProgressDistance;

            var childWidth = wrappedChild.ActualWidth;
            var childHeight = wrappedChild.ActualHeight;

            translateX = childPoint.X - ExtendedGeometry.PathOffset.X;
            translateY = childPoint.Y - ExtendedGeometry.PathOffset.Y;

            //center align child
            translateX -= childWidth / 2.0;
            translateY -= childHeight / 2.0;

            wrapper.SetTransformCenter(childWidth / 2.0, childHeight / 2.0);

            if (smooth)
            {
                var childTranslationSmoothing = GetTranslationSmoothing((UIElement)wrapper.Content);
                if (!double.IsNaN(childTranslationSmoothing))
                    translationSmoothing = childTranslationSmoothing;
            }

            if (smooth && !double.IsNaN(progressDistance) && translationSmoothing > 0 && progressDistance > 0 && !wrapper.NoTranslateSmoothForced)
            {
                wrapper.TranslateX = (wrapper.TranslateX * translationSmoothing * 0.2 / progressDistance + translateX) / (translationSmoothing * 0.2 / progressDistance + 1);
                wrapper.TranslateY = (wrapper.TranslateY * translationSmoothing * 0.2 / progressDistance + translateY) / (translationSmoothing * 0.2 / progressDistance + 1);
            }
            else
            {
                wrapper.TranslateX = translateX;
                wrapper.TranslateY = translateY;
                wrapper.NoTranslateSmoothForced = false;
            }
        }

        #endregion
    }
}
